<?php
/**
 * Smart Library - Smart Framework
 * http://smart-framework.highet.eu
 *
 * @category   SmartL
 * @package    SmartL_Zend
 * @subpackage Controller
 * @copyright  Copyright (c) 2005-2008 Smart Framework team - Stepan Sindelar, Vojtech Vit
 * @license    http://www.opensource.org/licenses/bsd-license.php	New BSD License
 */

/**
 * @see SmartL_Acl
 */
require_once 'SmartL/Acl.php';
/**
 * @see SmartL_Application
 */
require_once 'SmartL/Application.php';
/**
 * @see SmartL_Zend_Controller_Action_Config
 */
require_once 'SmartL/Zend/Controller/Action/Config.php';
/**
 * @see SmartL_Zend_View
 */
require_once 'SmartL/Zend/View.php';
/**
 * @see Zend_Auth
 */
require_once 'Zend/Auth.php';
/**
 * @see Zend_Controller_Action
 */
require_once 'Zend/Controller/Action.php';
/**
 * @see Zend_Controller_Action_Helper_ViewRenderer
 */
require_once 'Zend/Controller/Action/Helper/ViewRenderer.php';
/**
 * @see SmartL_Zend_Translate
 */
require_once 'SmartL/Zend/Translate.php';

/**
 * Extended action loads it's own configuration, initializes it's own
 * SmartL_Zend_View and loads translation file for requested action.
 * 
 * View and configuration are initialized by init() method.
 * Translation file is assigned to view in preDispatch. 
 * 
 * @todo 12 : Co kdyby si konfiguraci volal od SmartL_Application::getControllerConfig($name)
 * 
 * @package SmartL_Zend 
 * @subpackage Controller
 */
abstract class SmartL_Zend_Controller_Action extends Zend_Controller_Action {
	/**
	 * Controllers configuration directory (relative from configs dir)
	 */
	const CONFIG_CONTROLLERS_DIR = "Controller";
	/**
	 * Configuration files extension
	 */
	const CONFIG_EXTENSION = ".xml";
	
	/**
	 * SmartL_Application class
	 * 
	 * @var SmartL_Application
	 */
	protected $application = null;
	/**
	 * Controller configuration
	 * 
	 * @var SmartL_Zend_Controller_Action_Config
	 */
	protected $config = null;
	/**
	 * Avaiable static translation sources for this controller (action).
	 * This array is indexed by translation IDs given in configuration of
	 * particular controller. 
	 *
	 * @var array[string Translation id] => Zend_Translate
	 */
	protected $translations = null;
	/**
	 * Actual controller name
	 *
	 * @var string
	 */
	private $controllerName = null;
	
	public function __construct(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, array $invokeArgs = array(), SmartL_Application $application = null) {		
		// Set local reference to SmartL_Application class instance
		if ($application == null) {
			$this->application = SmartL_Application::getInstance ();
		} else {
			$this->application = $application;
		}
		parent::__construct ( $request, $response, $invokeArgs );
	}
	
	/**
	 * Initializes local variables, loads controller configuration and
	 * authorizes user to use this controller.
	 */
	public function init() {
		//SmartL helpers
		$this->_helper->addPath ( "SmartL/Zend/Controller/Action/Helper", "SmartL_Zend_Controller_Action_Helper" );
		
		// Initialize view
		$this->_initView ();
		
		// Load controller configuration
		$this->_loadConfig ();
		
		// Authorize user to use this controller
		$this->_authorizeUserController ();
	}
	/**
	 * Pre-dispatch routines authorize user to use requested action and load
	 * that action configuration.
	 * 
	 */
	public function preDispatch() {
		$actionName = $this->getRequest ()->getActionName ();	
		
		// Renames action name with dashes (like some-action) to it's correct
		// form (like someAction)
		$actionName = $this->_renameAction ( $actionName );
		
		// Load action configuration
		$actionConfig = $this->_loadActionConfig ( $actionName );		
		
		// Load action translation files
		if ($actionConfig !== false) {
			$this->_initTranslations ( $actionName, $actionConfig );
		}
		
		// Authorize user to use requested action
		$this->_authorizeUserAction ( $actionName );
	}
	/**
	 * Overrides parent method to implement caching
	 *
	 * @param string $action Action method's name.
	 */
	public function dispatch($action) {
		$actionName = substr ( $action, 0, strlen ( $action ) - strlen ( 'action' ) );
		$cacheEnabled = $this->_shouldBeCached ( $actionName );
		if ($cacheEnabled) {
			$cacheName = $this->application->getActionCacheName ();
			$cache = $this->application->getCacheManager ()->getAdapter ( $cacheName );
			$cacheId = $this->_getCacheId ( $actionName );
			if ($cache !== null and ($cachedData = $cache->load ( $cacheId ))) {
				$this->_helper->viewRenderer->setNoRender ();
				$this->_helper->notifyPreDispatch ();
				$this->_authorizeUserAction ( $this->getRequest ()->getActionName () ); //beacuse preDispatch won't be called.			
				$this->getResponse ()->appendBody ( $cachedData, $this->_helper->viewRenderer->getResponseSegment () );
				$this->_helper->notifyPostDispatch ();
				if (class_exists ( 'SmartL_Profiler' ) and SmartL_Profiler::getInstance ()->isEnabled ()) {
					SmartL_Profiler::getInstance ()->addCacheInfo ( 'action ' . $actionName . ' ' . get_class ( $this ) );
				}
			} else {
				parent::dispatch ( $action );
				$cache->save ( $this->getResponse ()->getBody (), $cacheId, 
					array_merge($this->config->getAction($actionName)->getCacheInfo()->getCacheTags(),array($action)) );
			}
		} else {
			parent::dispatch ( $action );
		}
	}
	/**
	 * Returns controller configuration.
	 * 
	 * @return SmartL_Zend_Controller_Action_Config
	 */
	public function getConfig() {
		return $this->config;
	}
	/**
	 * Creates new SmartL_Zend_View, saves it into $this->view, sets 
	 * it's basePath and assigns following variables to it:
	 * 
	 * string  templateName   Actual template name
	 * string  skinName       Actual skin name
	 * string  langName       Current language name
	 * string  langId         Current language id
	 * SmartL_Application_Config_LanguageInfo lang	Current language.
	 * bool    debugMode      DebugMode
	 * string  moduleName     Requested module name
	 * string  controllerName Requested controller name
	 * string  actionName     Requested action name
	 * mixed   user           User identity (return value of Zend_Auth::getIdentity())
	 * string  protocol       Actually used protocol
	 * string  hostName       Host name
	 * string  baseUrl        Url to project location
	 * string  fullBaseUrl    Url to project location icluding protocol and host name
	 * string  publicUrl      Url to public directory
	 * string  templateUrl    Url to actual template public directory
	 * string  skinUrl        Url to actual skin public directory
	 *  
	 */
	private function _initView() {
		$app = $this->application;
		$ds = DIRECTORY_SEPARATOR;
		
		$view = new SmartL_Zend_View ( array (), $app );
		$this->view = $view;
		if ($app->getActualTemplate () !== null) {
			$view->setBasePath ( $app->getBasePath () . $ds . $app->getActualModule ()->getViewsDir (), null, $app->getActualTemplate ()->getName () );
		} else {
			$view->setBasePath ( $app->getBasePath () . $ds . $app->getActualModule ()->getViewsDir (), null );
		}
		
		// Assign basic variables to the view
		if ($app->getActualTemplate () !== null) {
			$templateName = $view->templateName = $app->getActualTemplate ()->getName ();
			$view->skinName = $app->getActualTemplate ()->getActualSkin ();
		} else {
			$templateName = '';
			$view->templateName = null;
			$view->skinName = '';
		}
		// @todo 13 : Jazyků může být víc - bude stačit ten "hlavní"?
		$langs = $app->getLanguages ();
		$view->lang = $langs [0];
		$view->langName = $langs [0]->getName ();
		$view->langId = $langs [0]->getId ();
		$view->debugMode = $app->isDebugMode ();
		$view->moduleName = $this->getRequest ()->getModuleName ();
		$view->controllerName = $this->getRequest ()->getControllerName ();
		$view->user = Zend_Auth::getInstance ()->getIdentity ();
		
		$view->protocol = $app->getActualProtocol ();
		$view->hostName = $app->getHostName ();
		$view->baseUrl = $app->getBaseUrl ();
		$view->fullBaseUrl = $view->protocol . $view->hostName . "/" . $view->baseUrl;
		
		// @todo 19 : Změnit zdroj na getPublicUrl (díky mode rewrite se může nastavit jiný)
		$view->publicUrl = $view->fullBaseUrl . "/" . $app->getPublicDir () . "/";
		// @todo 20 : Změnit zdroj na konfiguraci šablony (díky mode rewrite se může nastavit hezčí adresa) NEBO alespoň podporovat oba způsoby (aby se to v konfiguraci nemuselo zadávat)
		$view->templateUrl = $view->publicUrl . strtolower ( $templateName ) . "/";
		// @todo 21 : Změnit zdroj na konfiguraci skinu (díky mode rewrite se může nastavit hezčí adresa) NEBO alespoň podporovat oba způsoby (aby se to v konfiguraci nemuselo zadávat)
		$view->skinUrl = $view->templateUrl . $view->skinName . "/";
		
		// Initialize the ViewRenderer helper
		if ($this->_helper->hasHelper ( 'viewRenderer' )) {
			$viewRenderer = $this->_helper->getHelper ( 'viewRenderer' );
			$viewRenderer->setView ( $view );
		} else {
			$viewRenderer = new Zend_Controller_Action_Helper_ViewRenderer ( );
			$viewRenderer->setView ( $view );
			$this->_helper->addHelper ( $viewRenderer );
		}
		// Set doctype to XHTML to make generated forms XHTML compliant.
		$viewRenderer->view->doctype ( 'XHTML1_TRANSITIONAL' );
	}
	/**
	 * Renames action name with dashes (like some-action - used in URLs) to 
	 * it's correct form (like someAction)
	 *
	 * @param string $actionName Action name with dashes
	 * @return string Correct action name
	 */
	private function _renameAction($actionName) {
		// @todo 14 : Je tento řádek nutný? Přijde mi trochu nečekaný z pohledu "plánovače URL"
		if ($actionName === null)
			$actionName = 'index';
		$words = explode ( '-', $actionName );
		$actionName = '';
		foreach ( $words as $word ) {
			if ($actionName != '')
				$word = ucfirst ( $word );
			$actionName .= $word;
		}
		return $actionName;
	}
	/**
	 * Loads and returns actual controller's config (if it exists).
	 * 
	 * @return SmartL_Controller_Action_Config|null Controller config or null
	 */
	private function _loadConfig() {
		$ds = DIRECTORY_SEPARATOR;
		$controllerName = substr ( get_class ( $this ), 0, strlen ( get_class ( $this ) ) - strlen ( "Controller" ) );
		// Remove all class prefixes
		while ( strpos ( $controllerName, "_" ) !== false ) {
			$controllerName = strstr ( $controllerName, "_" );
			$controllerName = substr ( $controllerName, 1 );
		}
		$this->controllerName = $controllerName;
		$filename = $this->application->getBasePath () . $ds . $this->application->getConfigPath () . $ds . $this->application->getActualModule ()->getName () . $ds . self::CONFIG_CONTROLLERS_DIR . $ds . $controllerName . self::CONFIG_EXTENSION;
		
		$config = new SmartL_Zend_Controller_Action_Config ( );
		
		// If config file doesn't exist, nothing special happens. Method just 
		// returns an "empty" config object.    
		if (file_exists ( $filename )) {
			$config->load ( $filename );
			// Extends controller config by its template specific variations
			if ($this->application->getActualTemplate () !== null) {
				$filename = $this->application->getBasePath () . $ds . $this->application->getConfigPath () . $ds . $this->application->getActualModule ()->getName () . $ds . self::CONFIG_CONTROLLERS_DIR . $ds . $this->application->getActualTemplate ()->getName () . $ds . $controllerName . self::CONFIG_EXTENSION;
				if (file_exists ( $filename )) {
					$config->extendBy ( $filename );
				}
			}
		}
		
		$this->config = $config;
	}
	/**
	 * Loads specified action configuration.
	 *
	 * @param string $actionName Action name
	 * @return SmartL_Controller_Action_Config_ActionInfo|bool Action config 
	 *          class or False, if it doesn't exist
	 */
	private function _loadActionConfig($actionName) {
		if ($this->config !== null and $this->config->hasActionConfig ( $actionName )) {
			return $this->config->getAction ( $actionName );
		}
		return false;
	}
	/**
	 * Loads translation files given in config, saves them into $this->translations
	 * and initializes translation view helper with this file and actual language
	 * from SmartL_Application. 
	 * 
	 * @todo 15 : Okomentovat inline komentáři co se děje uvnitř této metody.
	 * @param string $actionName Action name
	 * @param SmartL_Zend_Controller_Action_Config_ActionInfo $actionInfo Action config class.
	 */
	private function _initTranslations($actionName, SmartL_Zend_Controller_Action_Config_ActionInfo $actionInfo) {
		$ds = DIRECTORY_SEPARATOR;
		$translations = $actionInfo->getTranslations ();
		if ($this->application->getActualTemplate () !== null) {
			$templateName = $this->application->getActualTemplate ()->getName ();
		} else {
			$templateName = '';
		}
		
		if (isset ( $translations ['default'] )) {
			$defaultTranslation = $translations ['default'];
			if ($defaultTranslation->getFile () != null) {
				$filename = $this->application->getbasePath () . $ds . $defaultTranslation->getFile ();
			} else {
				$filename = $this->application->getbasePath () . $ds . $this->application->getTranslationsDir () . $ds . $templateName . $ds . $this->controllerName . $ds . $actionName . "." . $defaultTranslation->getExtension ();
			}
			if (! file_exists ( $filename )) {
				require_once 'SmartL/Zend/Controller/Action/Exception.php';
				throw new SmartL_Zend_Controller_Action_Exception ( "Translation file \"{$filename}\" was not found" );
			}
			
			$translate = SmartL_Zend_Translate::createTranslate ( $defaultTranslation->getAdapter (), $filename );
			$this->view->getHelper ( "translate" )->setTranslator ( $translate );
			$this->view->getHelper ( "translate" )->setLocale ( $translate->getLocale () );
			$this->translations ['default'] = $translate;
		}

		foreach ( $translations as $id => $data ) {
			if ($id == "default")
				continue;
			$translate = SmartL_Zend_Translate::createTranslate ( $data->getAdapter (), $this->application->getBasePath () . $ds . $this->application->getTranslationsDir () . $ds . $data->getFile () );
			$this->view->assign ( $id, $translate );
			$this->translations [$id] = $translate;
		}
	}
	/**
	 * Redirects to "access denied controller and action" (if specified) or
	 * throws SmartL_Application_AccessException.
	 * 
	 */
	private function _redirectToAccessDenied() {
		$info = $this->application->getAccessDeniedRequestInfo ();
		
		if ($info != null) {
			/*$this->_redirect("http://" . $this->application->getHostName()
                . $this->view->url(array("controller" => $info->getController()
                                       , "action" => $info->getAction())));*/
			$this->_helper->redirector->goto ( $info->getAction (), $info->getController () );
		} else {
			require_once 'SmartL/Application/AccessException.php';
			throw new SmartL_Application_AccessException ( "Access to requested action was denied" );
		}
	}
	/**
	 * Checks if actual user is allowed to use this controller. If
	 * not, it redirects to Access Denied controller and action (if specified)
	 * or throws SmartL_Application_AccessException.
	 */
	private function _authorizeUserController() {
		$isAuthorized = false;
		foreach ( $this->application->getUserRoles () as $role ) {
			if (SmartL_Acl::isAuthorized ( $role, $this->config->getAccessPermissions (), $this->config->getAccessRestrictions () )) {
				$isAuthorized = true;
				break;
			}
		}
		
		if (! $isAuthorized) {
			$this->_redirectToAccessDenied ();
		}
	}
	/**
	 * Checks if actual user is allowed to use this controller's action. If
	 * not, it redirects to Access Denied controller and action (if specified)
	 * or throws SmartL_Application_AccessException. 
	 */
	private function _authorizeUserAction($actionName) {
		$isAuthorized = true;
		if (isset($this->config) and
			$this->config->hasActionConfig ( $actionName )) {
			$isAuthorized = false;
			foreach ( $this->application->getUserRoles () as $role ) {
				if (SmartL_Acl::isAuthorized ( $role, $this->config->getAction ( $actionName )->getAccessPermissions (), $this->config->getAction ( $actionName )->getAccessRestrictions () )) {
					$isAuthorized = true;
					break;
				}
			}
		}
		
		if (! $isAuthorized) {
			$this->_redirectToAccesDenied ();
		}
	}
	
	public function _getCacheId($actionName) {
		if (! $this->config->hasActionConfig ( $actionName ))
			return false;
		if ($this->config->getAction ( $actionName )->getCacheInfo () == null)
			return false;
		
		$idRules = $this->config->getAction ( $actionName )->getCacheInfo ()->getCacheIdRules ();
		if ($idRules ['notcacheWhenUserLogged'] and Zend_Auth::getInstance ()->hasIdentity ()) {
			return false;
		}
		$id = $actionName;
		if ($idRules ['useParams']) {
			$params = $this->getRequest ()->getParams ();
			$keys = array_keys ( $params );
			$params = array_merge ( $keys, $params );
			$id .= implode ( ';', $params );
		}
		if ($idRules ['useUserId']) {
			if ( Zend_Auth::getInstance()->hasIdentity() ) {
				$id .= $this->application->getLoginInstance ()->getUserId ();
			}
		}
		if ($idRules ['useLanguageId']) {
			$lang = $this->application->getLanguages ();
			$lang = $lang [0];
			$id .= $lang->getId ();
		}
		return md5 ( $id );
	}
	
	public function _shouldBeCached($actionName) {
		if ( empty($this->config) ) return false;
		if (! $this->config->hasActionConfig ( $actionName ))
			return false;
		if ($this->config->getAction ( $actionName )->getCacheInfo () == null)
			return false;
		$idRules = $this->config->getAction ( $actionName )->getCacheInfo ()->getCacheIdRules ();
		if ($idRules ['notcacheWhenUserLogged'] and Zend_Auth::getInstance ()->hasIdentity ())
			return false;
		if ($this->config->getAction ( $actionName )->getCacheInfo ()->isDisabled ())
			return false;
		if ( $this->application->getActionCacheName () === null ) return false;
		return ! $this->application->getCacheManager ()->getAdapter ( $this->application->getActionCacheName () )->isDisabled ();
	}
}
